﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Xml;
using System.Text.RegularExpressions;
using System.Diagnostics;

namespace BibMan.model.database.bibtex {
    [Serializable]
    public class BibItem : IEnumerable<KeyValuePair<String, String>>,  IComparable<BibItem>, ICloneable, IXmlSerializable {

        private String STOPREGEX = "[:]";
        private readonly String EXTRATAGS_XML = "ExtraTags";
        private readonly String EXPECTEDTAGS_XML = "ExpectedTags";
        private readonly String BIBTYPE_XML = "BibType";

        /// <summary>
        /// BibTex entry type, not to be confused with TagName.type.
        /// </summary>
        [XmlAttribute]
        public BibType BibType { get; set; }
        
        public String Id { get; set; }

        private Dictionary<TagName, String> expectedTags;
        private Dictionary<String, String> extraTags;
        
        public String this[TagName index] {
            get {
                // If the tag doesn't exist, return nothing (fail silent)
                String value;
                if (expectedTags.TryGetValue(index, out value))
                    return value;
                else
                    return String.Empty;
            }
            set {
                // Remove any existing tag before update
                expectedTags.Remove(index);
                // Only add non-empty values
                if (value != null && !value.Equals(String.Empty))
                    expectedTags.Add(index, value);
            }
        }
        public String this[String index] {
            get {
                index = Regex.Replace(index, "[:]", String.Empty);
                TagName type;
                // Parse the string to try and use it as an expected tag
                if (Enum.TryParse<TagName>(index, true, out type)) {
                    return this[type];
                } else {
                    // If not an expected tag, get the value as extra tag
                    // If the extra tag doesn't exist, return nothing (fail silent)
                    String value;
                    if (extraTags.TryGetValue(index, out value))
                        return value;
                    else return String.Empty;
                }
            }
            set {
                index = Regex.Replace(index, "[:]", String.Empty);
                TagName type;
                // Parse the string to try and use it as an expected tag
                if (Enum.TryParse<TagName>(index, true, out type)) {
                    this[type] = value;
                } else {
                    // Remove any existing tag before update
                    extraTags.Remove(index);
                    // Only add non-empty values
                    if (value != null && !value.Equals(String.Empty))
                        extraTags.Add(index, value);
                }
            }
        }

        [XmlIgnore]
        public List<TagName> TagNames {
            get {
                return expectedTags.Keys.OrderBy(e => e).ToList();
            }
        }
        [XmlIgnore]
        public List<String> ExtraTagNames {
            get {
                return extraTags.Keys.OrderBy(e => e).ToList();
            }
        }


        public void AddTag(String tagName, String content) {
            this[tagName] = content;
        }
        public void AddTag(TagName tagName, String content) {
            this[tagName] = content;
        }
        public void RemoveTag(TagName tagName) {
            if (expectedTags.ContainsKey(tagName))
                expectedTags.Remove(tagName);
        }
        public void RemoveTag(String tagName) {
            TagName type;
            if (Enum.TryParse<TagName>(tagName, true, out type))
                RemoveTag(type);
            else if (extraTags.ContainsKey(tagName))
                extraTags.Remove(tagName);
        }


        public BibItem() {
            this.BibType = BibType.misc;
            this.expectedTags = new Dictionary<TagName, String>();
            this.extraTags = new Dictionary<String, String>();
        }

        public String ToBibTex() {
            StringBuilder ret = new StringBuilder();
            ret.Append("@").Append(BibType);
            ret.Append("{").Append(Id).AppendLine(",");
            foreach (TagName tagName in expectedTags.Keys) {
                ret.Append("\t").Append(tagName).Append(" = {");
                ret.Append(expectedTags[tagName]).AppendLine("},");
            }
            foreach (String tagName in extraTags.Keys) {
                ret.Append("\t").Append(tagName).Append(" = {");
                ret.Append(extraTags[tagName]).AppendLine("},");
            }
            ret.Append("}");
            return ret.ToString();
        }

        public override string ToString() {
            return ToBibTex();
        }

        public static BibItem Parse(String BibTex) {
            BibItem ret;
            if (TryParse(BibTex, out ret)) {
                return ret;
            } else {
                throw new FormatException("BibTex format not recognised.");
            }
        }
        public static bool TryParse(String BibTex, out BibItem bibItem) {
            BibParser parser = new BibParser(BibTex);
            bibItem = parser.Parse();
            if (parser.ErrorText.Length == 0) return true;
            else return false;
        }

        public static List<TagName> GetExpectedTags(BibType bibType) {
            switch (bibType) {
                case BibType.article:
                    return new List<TagName>() { 
                        TagName.author, TagName.title, TagName.journal,
                        TagName.year, TagName.volume, TagName.number, 
                        TagName.pages, TagName.month, TagName.note,
                        TagName.key
                    };
                case BibType.book:
                    return new List<TagName>() {
                        TagName.author, TagName.editor, TagName.title,
                        TagName.publisher, TagName.year, TagName.volume,
                        TagName.number, TagName.series, TagName.address,
                        TagName.edition, TagName.month, TagName.note,
                        TagName.key
                    };
                case BibType.booklet:
                    return new List<TagName>() { 
                        TagName.title, TagName.author, TagName.howpublished,
                        TagName.address, TagName.month, TagName.year, 
                        TagName.note, TagName.key
                    };
                case BibType.conference:
                    goto case BibType.inproceedings;
                case BibType.inbook:
                    return new List<TagName>() { 
                        TagName.author, TagName.editor, TagName.title,
                        TagName.chapter, TagName.pages, TagName.publisher,
                        TagName.year, TagName.volume, TagName.number,
                        TagName.series, TagName.type, TagName.address,
                        TagName.edition, TagName.month, TagName.note, 
                        TagName.key
                    };
                case BibType.incollection:
                    return new List<TagName>() {
                        TagName.author, TagName.title, TagName.booktitle,
                        TagName.publisher, TagName.year, TagName.editor,
                        TagName.volume, TagName.number, TagName.series,
                        TagName.type, TagName.chapter, TagName.pages,
                        TagName.address, TagName.edition, TagName.month,
                        TagName.note, TagName.key
                    };
                case BibType.inproceedings:
                    return new List<TagName>() { 
                        TagName.author, TagName.title, TagName.booktitle,
                        TagName.year, TagName.editor, TagName.volume, 
                        TagName.number, TagName.series, TagName.pages, 
                        TagName.address, TagName.month, TagName.organization,
                        TagName.publisher, TagName.note, TagName.key
                    };
                case BibType.manual:
                    return new List<TagName>() { 
                        TagName.title, TagName.author, TagName.organization,
                        TagName.address, TagName.edition, TagName.month,
                        TagName.year, TagName.note, TagName.key
                    };
                case BibType.matersthesis:
                    goto case BibType.phdthesis;
                case BibType.misc:
                    return new List<TagName>() {
                        TagName.author, TagName.title, TagName.howpublished, 
                        TagName.month, TagName.year, TagName.note, 
                        TagName.key
                    };
                case BibType.phdthesis:
                    return new List<TagName>() { 
                        TagName.author, TagName.title, TagName.school,
                        TagName.year, TagName.type, TagName.address, 
                        TagName.month, TagName.note, TagName.key
                    };
                case BibType.proceedings:
                    return new List<TagName>() { 
                        TagName.title, TagName.year, TagName.editor, 
                        TagName.volume, TagName.number, TagName.series, 
                        TagName.address, TagName.month, TagName.publisher, 
                        TagName.organization, TagName.note, TagName.key
                    };
                case BibType.techreport:
                    return new List<TagName>() { 
                        TagName.author, TagName.title, TagName.institution,
                        TagName.year, TagName.type, TagName.number, 
                        TagName.address, TagName.month, TagName.note,
                        TagName.key
                    };
                case BibType.unpublished:
                    return new List<TagName>() { 
                        TagName.author, TagName.title, TagName.note,
                        TagName.month, TagName.year, TagName.key
                    };
                default: return new List<TagName>();
            }
        }

        public int CompareTo(BibItem other) {
            return this.Id.CompareTo(other.Id);
        }

        public object Clone() {
            BibItem ret = new BibItem();
            ret.BibType = BibType;
            ret.Id = Id;
            ret.expectedTags = new Dictionary<TagName, String>(expectedTags);
            ret.extraTags = new Dictionary<String, String>(extraTags);
            return ret;
        }

        public System.Xml.Schema.XmlSchema GetSchema() {
            return null;
        }

        public void ReadXml(System.Xml.XmlReader reader) {
            if (reader.IsEmptyElement) return;
            BibType = (BibType)Enum.Parse(typeof(BibType), reader.GetAttribute(BIBTYPE_XML));
            reader.ReadToFollowing(EXPECTEDTAGS_XML);
            reader.Read();
            while (reader.NodeType != System.Xml.XmlNodeType.EndElement) {
                this.AddTag(reader.Name, reader.ReadElementString());
            }
            reader.ReadToFollowing(EXTRATAGS_XML);
            reader.Read();
            while (reader.NodeType != System.Xml.XmlNodeType.EndElement) {
                this.AddTag(reader.Name, reader.ReadElementString());
            }
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer) {
            writer.WriteAttributeString(BIBTYPE_XML, BibType.ToString());
            writer.WriteStartElement(EXPECTEDTAGS_XML);
            foreach (KeyValuePair<TagName, String> kvp in expectedTags) {
                writer.WriteElementString(kvp.Key.ToString(), kvp.Value);
            }
            writer.WriteEndElement();
            writer.WriteStartElement(EXTRATAGS_XML);
            foreach (KeyValuePair<String, String> kvp in extraTags) {
                writer.WriteElementString(kvp.Key.ToString(), kvp.Value);
            }
            writer.WriteEndElement();
        }

        public IEnumerator<KeyValuePair<string, string>> GetEnumerator() {
            yield return new KeyValuePair<String,String>("BibType", BibType.ToString());
            yield return new KeyValuePair<String, String>("Id", Id);
            foreach (KeyValuePair<TagName, String> kvp in expectedTags) {
                yield return new KeyValuePair<String, String>(kvp.Key.ToString(), kvp.Value);
            }
            foreach (KeyValuePair<String, String> kvp in extraTags) {
                yield return kvp;
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }
    }
}
